/*
 * JStock - Free Stock Market Software
 * Copyright (C) 2012 Yan Cheng CHEOK <yccheok@yahoo.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package org.yccheok.jstock.gui;

import org.yccheok.jstock.alert.GoogleMail;
import org.yccheok.jstock.alert.GoogleCalendar;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.swing.event.*;
import com.nexes.wizard.*;
import javax.swing.*;
import java.awt.event.*;
import java.io.File;
import java.text.MessageFormat;
import javax.swing.table.*;
import java.util.*;
import org.yccheok.jstock.engine.*;
import org.yccheok.jstock.analysis.*;
import java.util.concurrent.*;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.yccheok.jstock.alert.SMSLimiter;
import org.yccheok.jstock.analysis.Indicator;
import org.yccheok.jstock.internationalization.GUIBundle;

/**
 *
 * @author  yccheok
 */
public class IndicatorScannerJPanel extends javax.swing.JPanel implements ChangeListener, org.yccheok.jstock.engine.Observer<Indicator, Boolean> {
    
    /** Creates new form IndicatorScannerJPanel */
    public IndicatorScannerJPanel() {
        initComponents();
        
        initTableHeaderToolTips();
        this.initGUIOptions();
        
        // Reader and writer locks, so that we can have a correct stop operation.
        java.util.concurrent.locks.ReadWriteLock readWriteLock = new java.util.concurrent.locks.ReentrantReadWriteLock();
        reader = readWriteLock.readLock();
        writer = readWriteLock.writeLock();

        // Get ready with all the data structures when pressing "start".
        initRealTimeStockMonitor(MainFrame.getInstance().getStockServerFactories());
        initStockHistoryMonitor(MainFrame.getInstance().getStockServerFactories());
        initAlertDataStructures();
        initCompleteProgressDataStructures();
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        jPanel1 = new javax.swing.JPanel();
        jButton1 = new javax.swing.JButton();
        jButton2 = new javax.swing.JButton();
        jPanel2 = new javax.swing.JPanel();
        jScrollPane1 = new javax.swing.JScrollPane();
        jTable1 = new javax.swing.JTable();

        setLayout(new java.awt.BorderLayout(5, 5));

        jButton1.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/player_play.png"))); // NOI18N
        java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui"); // NOI18N
        jButton1.setText(bundle.getString("IndicatorScannerJPanel_Scan...")); // NOI18N
        jButton1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButton1ActionPerformed(evt);
            }
        });
        jPanel1.add(jButton1);

        jButton2.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/stop.png"))); // NOI18N
        jButton2.setText(bundle.getString("IndicatorScannerJPanel_Stop")); // NOI18N
        jButton2.setEnabled(false);
        jButton2.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButton2ActionPerformed(evt);
            }
        });
        jPanel1.add(jButton2);

        add(jPanel1, java.awt.BorderLayout.SOUTH);

        jPanel2.setBorder(javax.swing.BorderFactory.createTitledBorder(bundle.getString("IndicatorScannerJPanel_IndicatorScanResult"))); // NOI18N
        jPanel2.setLayout(new java.awt.BorderLayout());

        jTable1.setAutoCreateRowSorter(true);
        jTable1.setFont(jTable1.getFont().deriveFont(jTable1.getFont().getStyle() | java.awt.Font.BOLD, jTable1.getFont().getSize()+1));
        jTable1.setModel(new IndicatorTableModel());
        jTable1.setAutoResizeMode(javax.swing.JTable.AUTO_RESIZE_OFF);
        this.jTable1.setDefaultRenderer(Number.class, new StockTableCellRenderer());
        this.jTable1.setDefaultRenderer(Double.class, new StockTableCellRenderer());
        this.jTable1.setDefaultRenderer(Object.class, new StockTableCellRenderer());

        this.jTable1.getTableHeader().addMouseListener(new TableColumnSelectionPopupListener(2));
        this.jTable1.addMouseListener(new TableRowPopupListener());
        this.jTable1.addKeyListener(new TableKeyEventListener());
        jScrollPane1.setViewportView(jTable1);

        jPanel2.add(jScrollPane1, java.awt.BorderLayout.CENTER);

        add(jPanel2, java.awt.BorderLayout.CENTER);
    }// </editor-fold>//GEN-END:initComponents

    private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton2ActionPerformed
        stop();
    }//GEN-LAST:event_jButton2ActionPerformed

    private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton1ActionPerformed
        // this.startScanThread must be null, as "stop" button must be pressed
        // before we can press "start" button.
        assert(this.startScanThread == null);

        stop_button_pressed = false;

        final MainFrame m = MainFrame.getInstance();
        
        if (m.getStockInfoDatabase() == null) {
            javax.swing.JOptionPane.showMessageDialog(this, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/messages").getString("info_message_we_havent_connected_to_stock_server"), java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/messages").getString("info_title_we_havent_connected_to_stock_server"), javax.swing.JOptionPane.INFORMATION_MESSAGE);
            return;
        }

        // Reset dirty flag, to allow background thread to show indicator on
        // the table.
        allowIndicatorShown = true;
        
        initWizardDialog();
        
        int ret = wizard.showModalDialog(680, -1);

        if (ret != Wizard.FINISH_RETURN_CODE)
            return;

        final WizardModel wizardModel = wizard.getModel();

        this.startScanThread = getStartScanThread(wizardModel);
        this.startScanThread.start();

        jButton1.setEnabled(false);
        jButton2.setEnabled(true);

        m.setStatusBar(true, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui").getString("IndicatorScannerJPanel_IndicatorScannerIsScanning..."));
    }//GEN-LAST:event_jButton1ActionPerformed

    /**
     * Initialize GUI options of this indicator scanner panel.
     */
    public void initGUIOptions() {
        File f = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config" + File.separator + "indicatorscannerjpanel.xml");
        GUIOptions guiOptions = org.yccheok.jstock.gui.Utils.fromXML(GUIOptions.class, f);

        if (guiOptions == null)
        {
            // When user launches JStock for first time, we will help him to
            // turn off the following column(s), as we feel those information
            // is redundant. If they wish to view those information, they have
            // to turn it on explicitly.
            JTableUtilities.removeTableColumn(jTable1, GUIBundle.getString("MainFrame_Open"));
            return;
        }

        if (guiOptions.getJTableOptionsSize() <= 0)
        {
            // When user launches JStock for first time, we will help him to
            // turn off the following column(s), as we feel those information
            // is redundant. If they wish to view those information, they have
            // to turn it on explicitly.
            JTableUtilities.removeTableColumn(jTable1, GUIBundle.getString("MainFrame_Open"));
            return;
        }

        /* Set Table Settings */
        JTableUtilities.setJTableOptions(jTable1,guiOptions.getJTableOptions(0));
	}

    public boolean saveGUIOptions() {
        if(Utils.createCompleteDirectoryHierarchyIfDoesNotExist(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config") == false)
        {
            return false;
        }

        final GUIOptions.JTableOptions jTableOptions = new GUIOptions.JTableOptions();

        final int count = this.jTable1.getColumnCount();
        for (int i = 0; i < count; i++) {
            final String name = this.jTable1.getColumnName(i);
            final TableColumn column = jTable1.getColumnModel().getColumn(i);
            jTableOptions.addColumnOption(GUIOptions.JTableOptions.ColumnOption.newInstance(name, column.getWidth()));
        }

        final GUIOptions guiOptions = new GUIOptions();
        guiOptions.addJTableOptions(jTableOptions);

        File f = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config" + File.separator + "indicatorscannerjpanel.xml");
        return org.yccheok.jstock.gui.Utils.toXML(guiOptions, f);
    }

    // Time consuming method. It involves file I/O reading (getOperatorIndicator).
    private void initOperatorIndicators(WizardModel wizardModel)
    {
        // Clear the previous operator indicators.
        this.operatorIndicators.clear();

        WizardPanelDescriptor wizardPanelDescriptor0 = wizardModel.getPanelDescriptor(WizardSelectStockDescriptor.IDENTIFIER);
        WizardSelectStockJPanel wizardSelectStockJPanel = (WizardSelectStockJPanel)wizardPanelDescriptor0.getPanelComponent();
        WizardPanelDescriptor wizardPanelDescriptor1 = wizardModel.getPanelDescriptor(WizardSelectIndicatorDescriptor.IDENTIFIER);
        WizardSelectIndicatorJPanel wizardSelectIndicatorJPanel = (WizardSelectIndicatorJPanel)wizardPanelDescriptor1.getPanelComponent();

        final MainFrame m = MainFrame.getInstance();
        final IndicatorProjectManager alertIndicatorProjectManager = m.getAlertIndicatorProjectManager();
        java.util.List<String> projects = wizardSelectIndicatorJPanel.getSelectedProjects();
        java.util.List<StockInfo> stockInfos = wizardSelectStockJPanel.getSelectedStockInfos();

        for (final StockInfo stockInfo : stockInfos) {
            if (this.stop_button_pressed) {
                return;
            }

            final java.util.List<OperatorIndicator> result = new java.util.ArrayList<OperatorIndicator>();

            this.operatorIndicators.put(stockInfo.code, result);

            for (String project : projects) {
                final OperatorIndicator operatorIndicator = alertIndicatorProjectManager.getOperatorIndicator(project);

                if (operatorIndicator != null) {
                    final Stock stock = Utils.getEmptyStock(stockInfo);

                    operatorIndicator.setStock(stock);

                    result.add(operatorIndicator);
                }
                try {
                    /* Some users with low computer spec, complain that their CPUs usage are high.
                     * My experience is that, 200ms sleep time will be enough to rest their CPUs.
                     * I am not too sure about 50ms. Let's just wait and see...
                     * When user runs this Indicator Scanner, he is expecting that he needs to wait.
                     * So, it doesn't matter that we let him "wait" for extra 50ms seconds every round.
                     * Some more, he shall feel more happy, to see his computer more responsive.
                     */
                    Thread.sleep(50);
                } catch (InterruptedException ex) {
                    log.error(null, ex);
                    break;
                }
            }   /* for(String project : projects) */

            this.submitOperatorIndicatorToMonitor(result);
        }   /* for(String code : codes) */
    }

    private void submitOperatorIndicatorToMonitor(java.util.List<OperatorIndicator> indicators)
    {
        Duration historyDuration = Duration.getTodayDurationByDays(0);

        for (OperatorIndicator operatorIndicator : indicators)
        {
            historyDuration = historyDuration.getUnionDuration(operatorIndicator.getNeededStockHistoryDuration());
        }

        // Duration must be initialized, before codes being added.
        this.stockHistoryMonitor.setDuration(historyDuration);

        boolean done = true;
        for (OperatorIndicator operatorIndicator : indicators)
        {
            /* Hacking way to make startScanThread stop within a very short time. */
            if (this.stop_button_pressed) {
                return;
            }

            if (operatorIndicator.isStockHistoryCalculationDone() == false)
            {
                done = false;

                // Early break. We will let history monitor to perform pre-calculation.
                break;
            }
            else
            {
                operatorIndicator.preCalculate();
            }
        }

        if (indicators.size() > 0) {
            // All indicator in indicators, will be having same code.
            final Code code = indicators.get(0).getStock().getCode();
            if (done)
            {
                // Perform real time monitoring, for the code with history information.
                realTimeStockMonitor.addStockCode(code);
                realTimeStockMonitor.refresh();
            }
            else
            {
                // Try to load history from disk first.
                StockHistoryServer stockHistoryServer = this.stockHistoryMonitor.getStockHistoryServer(code);
                if (stockHistoryServer == null) {
                    this.stockHistoryMonitor.addStockCode(code);
                } else {
                    processHistory(code, stockHistoryServer);
                }
            }
        }
    }

    @Override
    public void update(final Indicator indicator, Boolean result) {
        // Use local variables, to ensure we do not consume the newly
        // initialized variables after stop(). The code should be placed before
        // "if (this.stop_button_pressed)" check.
        ExecutorService _emailAlertPool = null;
        ExecutorService _smsAlertPool = null;
        ExecutorService _systemTrayAlertPool = null;

        // There are 2 reasons why we are applying lock right here.
        // 1) Ensure visibility, as we do not apply volatile in all member
        //    variables.
        // 2) Make sure it is mutual exclusive with stop operation.
        reader.lock();
        try {
            _emailAlertPool = this.emailAlertPool;
            _smsAlertPool = this.smsAlertPool;
            _systemTrayAlertPool = this.systemTrayAlertPool;

            if (this.stop_button_pressed) {
                return;
            }
        } finally {
            reader.unlock();
        }

        final boolean flag = result;

        if (flag == false)
        {
            removeIndicatorFromTable(indicator);
            return;
        }

        addIndicatorToTable(indicator);

        final MainFrame m = MainFrame.getInstance();
        final JStockOptions jStockOptions = m.getJStockOptions();

        if (jStockOptions.isPopupMessage()) {
            final Runnable r = new Runnable() {
                @Override
                public void run() {
                    final Stock stock = indicator.getStock();
                    final double price = stock.getLastPrice();
                    final String template = GUIBundle.getString("IndicatorScannerJPanel_Hit_template");
                    final String message = MessageFormat.format(template, stock.getSymbol(), price, indicator.toString());

                    if (jStockOptions.isPopupMessage()) {
                        m.displayPopupMessage(stock.getSymbol().toString(), message);

                        if (jStockOptions.isSoundEnabled()) {
                            /* Non-blocking. */
                            Utils.playAlertSound();
                        }
                        try {
                            Thread.sleep(jStockOptions.getAlertSpeed() * 1000);
                        }
                        catch (InterruptedException exp) {
                            log.error(null, exp);
                        }
                    }
                }
            };

            try {
                _systemTrayAlertPool.submit(r);
            }
            catch (java.util.concurrent.RejectedExecutionException exp) {
                log.error(null, exp);
            }
        }   /* if (jStockOptions.isPopupMessage()) */

        // Sound alert hasn't been submitted to pop up message pool.
        if (jStockOptions.isPopupMessage() == false && jStockOptions.isSoundEnabled()) {
            final Runnable r = new Runnable() {
                @Override
                public void run() {
                    if (jStockOptions.isSoundEnabled()) {
                        /* Non-blocking. */
                        Utils.playAlertSound();

                        try {
                            Thread.sleep(jStockOptions.getAlertSpeed() * 1000);
                        }
                        catch (InterruptedException exp) {
                            log.error(null, exp);
                        }
                    }
                }
            };

            try {
                _systemTrayAlertPool.submit(r);
            }
            catch (java.util.concurrent.RejectedExecutionException exp) {
                log.error(null, exp);
            }
        }   /* if (this.jStockOptions.isSoundEnabled()) */

        if (jStockOptions.isSendEmail()) {
            final Runnable r = new Runnable() {
                @Override
                public void run() {
                    final Stock stock = indicator.getStock();
                    final double price = stock.getLastPrice();
                    final String template = GUIBundle.getString("IndicatorScannerJPanel_Hit_template");
                    final String title = MessageFormat.format(template, stock.getSymbol(), price, indicator.toString());
                    final String message = title + "\n(JStock)";

                    try {
                        final String email = Utils.decrypt(jStockOptions.getEmail());
                        final String CCEmail = Utils.decrypt(jStockOptions.getCCEmail());
                        GoogleMail.Send(email, Utils.decrypt(jStockOptions.getEmailPassword()), email + "@gmail.com", CCEmail, title, message);
                    } catch (AddressException exp) {
                        log.error(null, exp);
                    } catch (MessagingException exp) {
                        log.error(null, exp);
                    }
                }
            };

            try {
                _emailAlertPool.submit(r);
            }
            catch (java.util.concurrent.RejectedExecutionException exp) {
                log.error(null, exp);
            }
        }

        if (jStockOptions.isSMSEnabled()) {
            final Runnable r = new Runnable() {
                @Override
                public void run() {
                    final Stock stock = indicator.getStock();
                    final double price = stock.getLastPrice();
                    // Google Calendar only support "short" Chinese message.
                    // Usually our indicator name are quite long.
                    // We do not have any workaround except fall back to English message.
                    //
                    //final String template = GUIBundle.getString("IndicatorScannerJPanel_Hit_template");
                    final ResourceBundle bundle = ResourceBundle.getBundle("org.yccheok.jstock.data.gui", Locale.ENGLISH);
                    final String template = bundle.getString("IndicatorScannerJPanel_Hit_template");
                    final String message = MessageFormat.format(template, stock.getSymbol(), price, indicator.toString());

                    final String username = Utils.decrypt(jStockOptions.getGoogleCalendarUsername());
                    if (SMSLimiter.INSTANCE.isSMSAllowed()) {
                        final boolean status = GoogleCalendar.SMS(username, Utils.decrypt(jStockOptions.getGoogleCalendarPassword()), message);
                        if (status) {
                            SMSLimiter.INSTANCE.inc();
                        }
                    }
                }
            };

            try {
                _smsAlertPool.submit(r);
            }
            catch(java.util.concurrent.RejectedExecutionException exp) {
                log.error(null, exp);
            }
        }
    }

    private static class ColumnHeaderToolTips extends MouseMotionAdapter {

        // Current column whose tooltip is being displayed.
        // This variable is used to minimize the calls to setToolTipText().
        TableColumn curCol;
    
        // Maps TableColumn objects to tooltips
        Map<TableColumn, String> tips = new HashMap<TableColumn, String>();
    
        // If tooltip is null, removes any tooltip text.
        public void setToolTip(TableColumn col, String tooltip) {
            if (tooltip == null) {
                tips.remove(col);
            } else {
                tips.put(col, tooltip);
            }
        }
    
        @Override
        public void mouseMoved(MouseEvent evt) {
            TableColumn col = null;
            JTableHeader header = (JTableHeader)evt.getSource();
            JTable table = header.getTable();
            TableColumnModel colModel = table.getColumnModel();
            int vColIndex = colModel.getColumnIndexAtX(evt.getX());
    
            // Return if not clicked on any column header
            if (vColIndex >= 0) {
                col = colModel.getColumn(vColIndex);
            }
    
            if (col != curCol) {
                header.setToolTipText((String)tips.get(col));
                curCol = col;
            }
        }
    }
    
    private void initTableHeaderToolTips() {
        JTableHeader header = jTable1.getTableHeader();
    
        ColumnHeaderToolTips tips = new ColumnHeaderToolTips();
            
        header.addMouseMotionListener(tips);        
    }
    
    @Override
    public void stateChanged(javax.swing.event.ChangeEvent evt) {
    }    
    
    public void clear()
    {
        final MainFrame m = MainFrame.getInstance();
        this.initRealTimeStockMonitor(m.getStockServerFactories());
        this.initStockHistoryMonitor(m.getStockServerFactories());

        this.operatorIndicators.clear();
        // Ask help from dirty flag, so that background thread won't have
        // chance to show indicators on the table.
        allowIndicatorShown = false;
        
        removeAllIndicatorsFromTable();

        m.setStatusBar(false, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui").getString("IndicatorScannerJPanel_Connected"));
    }
    
    public void stop()
    {
        writer.lock();
        try {
            /* Hacking way to make startScanThread stop within a very short time. */
            stop_button_pressed = true;

            // We must ensure there is no reader locking mechanism within 
            // startScanThread. If not, deadlock might happen.
            final Thread thread = this.startScanThread;
            this.startScanThread = null;
            if (thread != null)
            {
                thread.interrupt();
                try {
                    thread.join();
                } catch (InterruptedException ex) {
                    log.error(null, ex);
                }
            }
            final MainFrame m = MainFrame.getInstance();
            this.initRealTimeStockMonitor(m.getStockServerFactories());
            this.initStockHistoryMonitor(m.getStockServerFactories());
            this.initAlertDataStructures();
            this.initCompleteProgressDataStructures();
        } finally {
            writer.unlock();
        }


        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                jButton1.setEnabled(true);
                jButton2.setEnabled(false);            
            }
            
        });

        MainFrame.getInstance().setStatusBar(false, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui").getString("IndicatorScannerJPanel_Connected"));
    }
    
    private void initWizardDialog() {
        final MainFrame m = MainFrame.getInstance();
        
        wizard = new Wizard(m);

        wizard.getDialog().setTitle(java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui").getString("IndicatorScannerJPanel_IndicatorScanningWizard"));
        wizard.getDialog().setResizable(false);
        
        WizardPanelDescriptor wizardSelectIndicatorDescriptor = new WizardSelectIndicatorDescriptor();
        wizard.registerWizardPanel(WizardSelectIndicatorDescriptor.IDENTIFIER, wizardSelectIndicatorDescriptor);
        
        // Quick hack. WizardSelectStockJPanel has no way to obtain MainFrame, during its construction
        // stage.
        WizardPanelDescriptor wizardSelectStockDescriptor = new WizardSelectStockDescriptor(m.getStockInfoDatabase());
        wizard.registerWizardPanel(WizardSelectStockDescriptor.IDENTIFIER, wizardSelectStockDescriptor);
        
        wizard.setCurrentPanel(WizardSelectIndicatorDescriptor.IDENTIFIER); 
 
        // Center to screen.
        wizard.getDialog().setLocationRelativeTo(null);
    }
    
    public void updateScanningSpeed(int speed) {
        this.realTimeStockMonitor.setDelay(speed);
    }

    public final void initStockHistoryMonitor(java.util.List<StockServerFactory> stockServerFactories) {
        final StockHistoryMonitor oldStockHistoryMonitor = stockHistoryMonitor;
        if (oldStockHistoryMonitor != null) {            
            Utils.getZoombiePool().execute(new Runnable() {
                @Override
                public void run() {
                    log.info("Prepare to shut down " + oldStockHistoryMonitor + "...");
                    oldStockHistoryMonitor.clearStockCodes();
                    oldStockHistoryMonitor.dettachAll();
                    oldStockHistoryMonitor.stop();
                    log.info("Shut down " + oldStockHistoryMonitor + " peacefully.");
                }
            });
        }

        this.stockHistoryMonitor = new StockHistoryMonitor(NUM_OF_THREADS_HISTORY_MONITOR);
        stockHistoryMonitor.setStockServerFactories(stockServerFactories);

        stockHistoryMonitor.attach(stockHistoryMonitorObserver);
        stockHistoryMonitor.setStockHistorySerializer(new StockHistorySerializer(Utils.getHistoryDirectory()));
    }

    private void processHistory(Code code, StockHistoryServer stockHistoryServer) {
        // Use local variables, to ensure we do not consume the newly
        // initialized variables after stop(). The code should be placed before
        // "if (this.stop_button_pressed)" check.
        Set<Code> _failedCodes = null;
        RealTimeStockMonitor _realTimeStockMonitor = null;

        // There are 2 reasons why we are applying lock right here.
        // 1) Ensure visibility, as we do not apply volatile in all member
        //    variables.
        // 2) Make sure it is mutual exclusive with stop operation.
        reader.lock();
        try {
            _failedCodes = this.failedCodes;
            _realTimeStockMonitor = this.realTimeStockMonitor;
            if (this.stop_button_pressed) {
                return;
            }
        } finally {
            reader.unlock();
        }

        final MainFrame m = MainFrame.getInstance();

        List<OperatorIndicator> indicators = this.operatorIndicators.get(code);
        if (indicators == null)
        {
            return;
        }

        if (stockHistoryServer == null)
        {
            _failedCodes.add(code);

            // Probably the network is down. Do not ever retry infinityly. Go 
            // green. :)
            //monitor.addStockCode(code);
            return;
        }

        _failedCodes.remove(code);

        Symbol symbol = m.getStockInfoDatabase().codeToSymbol(code);

        final String template = GUIBundle.getString("IndicatorScannerJPanel_IndicatorScannerFoundHistory_template");
        final String message = MessageFormat.format(template, symbol != null ? symbol : code, getCompleteScannedStocksPercentage());
        this.updateStatusBarIfStopButtonIsNotPressed(message);

        for (OperatorIndicator operatorIndicator : indicators)
        {
            if (operatorIndicator.isStockHistoryServerNeeded())
            {
                operatorIndicator.setStockHistoryServer(stockHistoryServer);
            }

            operatorIndicator.preCalculate();
        }

        // Perform real time monitoring, for the code with history information.
        _realTimeStockMonitor.addStockCode(code);
        _realTimeStockMonitor.refresh();
    }
    
    private void update(StockHistoryMonitor monitor, StockHistoryMonitor.StockHistoryRunnable runnable)
    {
        final Code code = runnable.getCode();
        final StockHistoryServer stockHistoryServer = runnable.getStockHistoryServer();
        processHistory(code, stockHistoryServer);
    }

    private org.yccheok.jstock.engine.Observer<StockHistoryMonitor, StockHistoryMonitor.StockHistoryRunnable> getStockHistoryMonitorObserver() {
        return new org.yccheok.jstock.engine.Observer<StockHistoryMonitor, StockHistoryMonitor.StockHistoryRunnable>() {
            @Override
            public void update(StockHistoryMonitor monitor, StockHistoryMonitor.StockHistoryRunnable runnable)
            {
                IndicatorScannerJPanel.this.update(monitor, runnable);
            }
        };
    }

    public void updatePrimaryStockServerFactory(java.util.List<StockServerFactory> stockServerFactories) {
        if (realTimeStockMonitor != null) {
            realTimeStockMonitor.setStockServerFactories(stockServerFactories);
        }

        if (stockHistoryMonitor != null) {
            stockHistoryMonitor.setStockServerFactories(stockServerFactories);
        }
    }

    // Initializes data structure used for complete progress calculation.
    private void initCompleteProgressDataStructures() {
        Set<Code> oldSuccessCodes = successCodes;
        Set<Code> oldFailedCodes = failedCodes;
        if (oldSuccessCodes != null) {
            oldSuccessCodes.clear();
        }
        if (oldFailedCodes != null) {
            oldFailedCodes.clear();
        }        
        successCodes = new java.util.concurrent.CopyOnWriteArraySet<Code>();
        failedCodes = new java.util.concurrent.CopyOnWriteArraySet<Code>();    
    }
    
    // Initializes data structure used for alerting purpose.
    private void initAlertDataStructures() {
        AlertStateManager oldAlertStateManager = alertStateManager;
        if (oldAlertStateManager != null) {
            oldAlertStateManager.dettachAll();
            oldAlertStateManager.clearState();
        }

        final ExecutorService oldSystemTrayAlertPool = systemTrayAlertPool;
        final ExecutorService oldEmailAlertPool = emailAlertPool;
        final ExecutorService oldSMSAlertPool = smsAlertPool;

        Utils.getZoombiePool().execute(new Runnable() {
            @Override
            public void run() {
                if (oldSystemTrayAlertPool != null) {
                    log.info("Prepare to shut down " + oldSystemTrayAlertPool + "...");
                    oldSystemTrayAlertPool.shutdownNow();
                    try {
                        oldSystemTrayAlertPool.awaitTermination(100, TimeUnit.DAYS);
                    } catch (InterruptedException exp) {
                        log.error(null, exp);
                    }
                    log.info("Shut down " + oldSystemTrayAlertPool + " peacefully.");

                    log.info("Prepare to shut down " + oldEmailAlertPool + "...");
                }

                if (oldEmailAlertPool != null) {
                    oldEmailAlertPool.shutdownNow();
                    try {
                        oldEmailAlertPool.awaitTermination(100, TimeUnit.DAYS);
                    } catch (InterruptedException exp) {
                        log.error(null, exp);
                    }
                    log.info("Shut down " + oldEmailAlertPool + " peacefully.");
                }

                if (oldSMSAlertPool != null) {
                    log.info("Prepare to shut down " + oldSMSAlertPool + "...");
                    oldSMSAlertPool.shutdownNow();
                    try {
                        oldSMSAlertPool.awaitTermination(100, TimeUnit.DAYS);
                    } catch (InterruptedException exp) {
                        log.error(null, exp);
                    }
                    log.info("Shut down " + oldSMSAlertPool + " peacefully.");
                }
            }
        });

        alertStateManager = new AlertStateManager();
        alertStateManager.attach(this);

        emailAlertPool = Executors.newFixedThreadPool(1);
        smsAlertPool = Executors.newFixedThreadPool(1);
        systemTrayAlertPool = Executors.newFixedThreadPool(1);
    }

    public final void initRealTimeStockMonitor(java.util.List<StockServerFactory> stockServerFactories) {
        final RealTimeStockMonitor oldRealTimeStockMonitor = this.realTimeStockMonitor;
        if (oldRealTimeStockMonitor != null) {            
            Utils.getZoombiePool().execute(new Runnable() {
                @Override
                public void run() {
                    log.info("Prepare to shut down " + oldRealTimeStockMonitor + "...");
                    oldRealTimeStockMonitor.clearStockCodes();
                    oldRealTimeStockMonitor.dettachAll();
                    oldRealTimeStockMonitor.stop();
                    log.info("Shut down " + oldRealTimeStockMonitor + " peacefully.");
                }
            });
        }
        
        this.realTimeStockMonitor = new RealTimeStockMonitor(4, 20, MainFrame.getInstance().getJStockOptions().getScanningSpeed());
        this.realTimeStockMonitor.setStockServerFactories(stockServerFactories);
        
        this.realTimeStockMonitor.attach(this.realTimeStockMonitorObserver);
    }
    
    // This is the workaround to overcome Erasure by generics. We are unable to make MainFrame to
    // two observers at the same time.
    private org.yccheok.jstock.engine.Observer<RealTimeStockMonitor, java.util.List<Stock>> getRealTimeStockMonitorObserver() {
        return new org.yccheok.jstock.engine.Observer<RealTimeStockMonitor, java.util.List<Stock>>() {
            @Override
            public void update(RealTimeStockMonitor monitor, java.util.List<Stock> stocks)
            {
                IndicatorScannerJPanel.this.update(monitor, stocks);
            }
        };
    }

    private void updateStatusBarIfStopButtonIsNotPressed(String message) {
        // Do we need to apply lock right here?
        if (this.stop_button_pressed) {
            return;
        }
        MainFrame.getInstance().setStatusBar(true, message);
    }

    private void update(RealTimeStockMonitor monitor, final java.util.List<Stock> stocks) {
        // Use local variables, to ensure we do not consume the newly
        // initialized variables after stop(). The code should be placed before
        // "if (this.stop_button_pressed)" check.
        AlertStateManager _alertStateManager = null;
        Set<Code> _successCodes = null;

        // There are 2 reasons why we are applying lock right here.
        // 1) Ensure visibility, as we do not apply volatile in all member 
        //    variables.
        // 2) Make sure it is mutual exclusive with stop operation.
        reader.lock();
        try {
            _alertStateManager = this.alertStateManager;
            _successCodes = this.successCodes;
            RealTimeStockMonitor _realTimeStockMonitor = this.realTimeStockMonitor;

            // Perform "_realTimeStockMonitor != monitor" check, to ensure we
            // are not using newly constructed realTimeStockMonitor. By just
            // merely using stop_button_pressed guard flag will not work as,
            //
            // 1) User presses on stop button.
            // 2) Old realTimeStockMonitor may stall.
            // 3) User presses on start button, and create new realTimeStockMonitor.
            // 4) stop_button_pressed has became true.
            // 5) Old realTimeStockMonitor resume. This may cause both old and
            //    new realTimeStockMonitor running.
            if (this.stop_button_pressed || _realTimeStockMonitor != monitor) {
                return;
            }
        } finally {
            reader.unlock();
        }

        final boolean isSymbolImmutable = org.yccheok.jstock.engine.Utils.isSymbolImmutable();
        for (int i = 0, size = stocks.size(); i < size; i++) {
            final Stock stock = stocks.get(i);
            Stock new_stock = stock;
            // Special handling for China stock market. Also, sometimes for
            // other countries, Yahoo will return empty string for their symbol.
            // We will fix it through offline database.
            if (isSymbolImmutable || new_stock.getSymbol().toString().isEmpty()) {                
                // Use local variable to ensure thread safety.
                final StockInfoDatabase stock_info_database = MainFrame.getInstance().getStockInfoDatabase();

                if (stock_info_database != null) {
                    final Symbol symbol = stock_info_database.codeToSymbol(stock.getCode());
                    if (symbol != null) {
                        new_stock = new_stock.deriveStock(symbol);
                    } else {
                        // Shouldn't be null. Let's give some warning on this.
                        log.error("Wrong stock code " + stock.getCode() + " given by stock server.");
                    }
                    if (stock != new_stock) {
                        stocks.set(i, new_stock);
                    }
                }   // if (symbol_database != null)
            } // if (org.yccheok.jstock.engine.Utils.isSymbolImmutable() || new_stock.getSymbol().toString().isEmpty())
        }   // for (int i = 0, size = stocks.size(); i < size; i++)

        if (stocks.size() > 0)
        {
            // We only print out the first stock, to avoid too many different
            // messages within a short duration.
            final String template = GUIBundle.getString("IndicatorScannerJPanel_IndicatorScannerIsScanning..._template");
            final String message = MessageFormat.format(template, stocks.get(0).getSymbol(), getCompleteScannedStocksPercentage());
            updateStatusBarIfStopButtonIsNotPressed(message);
        }

        for (Stock stock : stocks) {
            final java.util.List<OperatorIndicator> indicators = this.operatorIndicators.get(stock.getCode());
            
            if (indicators == null) {
                continue;
            }
            
            final JStockOptions jStockOptions = MainFrame.getInstance().getJStockOptions();

            if (jStockOptions.isSingleIndicatorAlert()) {
                for (OperatorIndicator indicator : indicators) {
                    indicator.setStock(stock);
                    _alertStateManager.alert(indicator);
                }
            }
            else
            {
                // Multiple indicators alert.
                for (OperatorIndicator indicator : indicators) {
                    indicator.setStock(stock);
                }

                _alertStateManager.alert(indicators);
            }

            // Indicates we has finished scanning this stock.
            _successCodes.add(stock.getCode());
        }
        
        // Display the same message again, so that we will get the most updated
        // complete percentage. Should we use back the same message template?
        if (stocks.size() > 0)
        {
            final String template = GUIBundle.getString("IndicatorScannerJPanel_IndicatorScannerIsScanning..._template");
            final String message = MessageFormat.format(template, stocks.get(0).getSymbol(), getCompleteScannedStocksPercentage());
            updateStatusBarIfStopButtonIsNotPressed(message);
        }
    }  

    private int getCompleteScannedStocksPercentage() {
        int expected = operatorIndicators.size();
        int failedCodesSize = failedCodes.size();
        int successCodesSize = successCodes.size();
        // As long as there is a least 1 success stock, we will consider failed
        // stocks as well. This is a very crude way, to determine Internet
        // connection is available, and the failed stocks are just caused by
        // incorrect stock codes.
        if ((successCodesSize > 0) && (expected > 0)) {
            return (successCodesSize + failedCodesSize) * 100 / expected;
        }
        return 0;
    }

    // Should we synchronized the jTable1, or post the job at GUI event dispatch
    // queue?
    private void addIndicatorToTable(final Indicator indicator) {
        final Runnable r = new Runnable() {
            @Override
            public void run() {          
                IndicatorTableModel tableModel = (IndicatorTableModel)jTable1.getModel();
                
                // Dirty way to prevent background thread from showing indicators
                // on the table.
                if (allowIndicatorShown) {
                    tableModel.addIndicator(indicator);
                }
           } 
        };
        
        SwingUtilities.invokeLater(r);
    }
    
    private void removeIndicatorFromTable(final Indicator indicator) {
        final Runnable r = new Runnable() {
            @Override
            public void run() {          
                IndicatorTableModel tableModel = (IndicatorTableModel)jTable1.getModel();
                tableModel.removeIndicator(indicator);
           } 
        };
        
        SwingUtilities.invokeLater(r);        
    }
    
    private void removeAllIndicatorsFromTable() {
        final Runnable r = new Runnable() {
            @Override
            public void run() {          
                IndicatorTableModel tableModel = (IndicatorTableModel)jTable1.getModel();
                tableModel.removeAll();
           } 
        };
        
        SwingUtilities.invokeLater(r);        
    }
    
    private class TableRowPopupListener extends MouseAdapter {
        
        @Override
        public void mouseClicked(MouseEvent evt) {
        }
        
        @Override
        public void mousePressed(MouseEvent e) {
            maybeShowPopup(e);
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            maybeShowPopup(e);
        }

        private void maybeShowPopup(MouseEvent e) {
            if (e.isPopupTrigger()) {
                if (jTable1.getSelectedRowCount() > 0) {
                    getMyJTablePopupMenu().show(e.getComponent(), e.getX(), e.getY());
                }
            }
        }
    }

    private ImageIcon getImageIcon(String imageIcon) {
        return new javax.swing.ImageIcon(getClass().getResource(imageIcon));
    }   

    private JPopupMenu getMyJTablePopupMenu() {
        JPopupMenu popup = new JPopupMenu();
        
        final MainFrame m = MainFrame.getInstance();
        
        javax.swing.JMenuItem menuItem = new JMenuItem(java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui").getString("IndicatorScannerJPanel_History..."), this.getImageIcon("/images/16x16/strokedocker.png"));
        
        menuItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                int rows[] = jTable1.getSelectedRows();
                final IndicatorTableModel tableModel = (IndicatorTableModel)jTable1.getModel();

                for (int row : rows) {
                    final int modelIndex = jTable1.convertRowIndexToModel(row);
                    final Indicator indicator = tableModel.getIndicator(modelIndex);
                    if (indicator != null) {
                        m.displayHistoryChart(indicator.getStock());
                    }
                }
            }
        });

        popup.add(menuItem);

        menuItem = new JMenuItem(GUIBundle.getString("IndicatorScannerJPanel_AddToRealTimeInfo"), this.getImageIcon("/images/16x16/add.png"));
        
        menuItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                int rows[] = jTable1.getSelectedRows();
                final IndicatorTableModel tableModel = (IndicatorTableModel)jTable1.getModel();

                for (int row : rows) {
                    final int modelIndex = jTable1.convertRowIndexToModel(row);
                    final Indicator indicator = tableModel.getIndicator(modelIndex);
                    if (indicator != null) {
                        m.addStockToTable(indicator.getStock());
                    }
                }
            }
        });

        popup.add(menuItem);

        if (jTable1.getSelectedRowCount() == 1) {
            popup.addSeparator();

            menuItem = new JMenuItem(GUIBundle.getString("IndicatorScannerJPanel_Buy..."), this.getImageIcon("/images/16x16/inbox.png"));

            menuItem.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent evt) {
                    final int row = jTable1.getSelectedRow();
                    final int modelIndex = jTable1.getRowSorter().convertRowIndexToModel(row);
                    final IndicatorTableModel tableModel = (IndicatorTableModel)jTable1.getModel();
                    final Indicator indicator = tableModel.getIndicator(modelIndex);
                    final Stock stock = indicator.getStock();
                    MainFrame.getInstance().getPortfolioManagementJPanel().showNewBuyTransactionJDialog(stock, stock.getLastPrice(), false);
                }
            });

            popup.add(menuItem);
        }

        return popup;
    }

    public void repaintTable() {
        jTable1.repaint();
    }
    
    public void clearTableSelection() {
        jTable1.getSelectionModel().clearSelection();
    }

    public boolean saveAsCSVFile(File file) {
        final TableModel tableModel = jTable1.getModel();
        // Unexpected result may happen while scanning is running, as table
        // model will be mutated during the middle of writting. Currently, we
        // do not have solution.
        final org.yccheok.jstock.file.Statements statements = org.yccheok.jstock.file.Statements.newInstanceFromTableModel(tableModel, false);
        assert(statements != null);
        return statements.saveAsCSVFile(file);
    }

    public boolean saveAsExcelFile(File file) {
        final TableModel tableModel = jTable1.getModel();
        // Unexpected result may happen while scanning is running, as table
        // model will be mutated during the middle of writting. Currently, we
        // do not have solution.
        final org.yccheok.jstock.file.Statements statements = org.yccheok.jstock.file.Statements.newInstanceFromTableModel(tableModel, false);
        assert(statements != null);
        return statements.saveAsExcelFile(file, GUIBundle.getString("IndicatorScannerJPanel_Title"));
    }

    private Thread getStartScanThread(final WizardModel wizardModel)
    {
        return new Thread(new Runnable() {
            @Override
            public void run() {
                WizardPanelDescriptor wizardPanelDescriptor0 = wizardModel.getPanelDescriptor(WizardSelectStockDescriptor.IDENTIFIER);
                WizardSelectStockJPanel wizardSelectStockJPanel = (WizardSelectStockJPanel)wizardPanelDescriptor0.getPanelComponent();

                if (wizardSelectStockJPanel.buildSelectedStockCodes() == false) {
                    // Unlikely.
                    log.error("Fail to build selected stock");
                    return;
                }

                removeAllIndicatorsFromTable();

                initOperatorIndicators(wizardModel);
            }
        });
    }

    private class TableKeyEventListener extends java.awt.event.KeyAdapter {
        @Override
        public void keyTyped(java.awt.event.KeyEvent e) {
            IndicatorScannerJPanel.this.clearTableSelection();
        }
    }
    
    public void refreshRealTimeStockMonitor() {
        RealTimeStockMonitor _realTimeStockMonitor = this.realTimeStockMonitor;
        if (_realTimeStockMonitor != null) {
            _realTimeStockMonitor.refresh();
        }
    }
    
    private Wizard wizard;
    private RealTimeStockMonitor realTimeStockMonitor;
    private final org.yccheok.jstock.engine.Observer<RealTimeStockMonitor, java.util.List<Stock>> realTimeStockMonitorObserver = this.getRealTimeStockMonitorObserver();
    private final java.util.Map<Code, java.util.List<OperatorIndicator>> operatorIndicators = new java.util.concurrent.ConcurrentHashMap<Code, java.util.List<OperatorIndicator>>();

    private Set<Code> successCodes;
    private Set<Code> failedCodes;

    private AlertStateManager alertStateManager;
    private ExecutorService emailAlertPool;
    private ExecutorService smsAlertPool;
    private ExecutorService systemTrayAlertPool;
    
    private final org.yccheok.jstock.engine.Observer<StockHistoryMonitor, StockHistoryMonitor.StockHistoryRunnable> stockHistoryMonitorObserver = this.getStockHistoryMonitorObserver();

    private StockHistoryMonitor stockHistoryMonitor = null;

    // Dirty flag to be used with clear method and start button method.
    // Ensure we have an instant way to prevent background thread from showing
    // indicators on the table, after we call clear method. 
    // This is a dirty way, but it just work :)
    private volatile Boolean allowIndicatorShown = true;

    // This boolean flag is important, as we are unable to use 
    // this.startScanThread != currentThread to stop an operation. We need to
    // stop several threads at the same time such as RealTimeStockMonitor.
    private volatile boolean stop_button_pressed = true;

    // Reader and writer locks, so that we can have a correct stop operation.
    private final java.util.concurrent.locks.Lock reader;
    private final java.util.concurrent.locks.Lock writer;

    // There isn't any need to make this thread volatile, as we do not use
    // this.startScanThread != currentThread technique to stop a thread. This is
    // just to be consistence across entire project.
    private volatile Thread startScanThread = null;

    private static final Log log = LogFactory.getLog(IndicatorScannerJPanel.class);

    private static final int NUM_OF_THREADS_HISTORY_MONITOR = 4;

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton jButton1;
    private javax.swing.JButton jButton2;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JPanel jPanel2;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JTable jTable1;
    // End of variables declaration//GEN-END:variables
    
}
